from xml.etree.ElementTree import parse, tostring
from data import createDb, committing, Colour, Place, Action, Arc, Linkage, \
    Execution, Transfer, Test, Transition, Inhibit, Alias, Field
import unittest
from sqlalchemy.engine import create_engine
from sqlalchemy.orm.session import sessionmaker
from collections import defaultdict

_namespaceMap = {'t': "http://www.temporel.org/temporel"}
#for ns in _namespaceMap.items(): register_namespace(*ns)

class _LifeCycle(object):
    def __init__(self, configuration, name, ephemeralPlace):
        assert name not in configuration.lifeCycles, name
        configuration.lifeCycles[name] = self
        self.name, self.ephemeralPlace, self.arcs  , self.placeNames = \
             name,      ephemeralPlace,      dict(),    set()
        self.placeNames.add(ephemeralPlace)
        
class _ProtoArc(object):
    def __init__(self, lifeCycle, name, fromNames=(), toName=None, negate=False):
        assert name not in lifeCycle.arcs, (lifeCycle.name, name)
        lifeCycle.arcs[name] = self
        fromNames = set(n or "NonExistent" for n in fromNames)
        if not fromNames: fromNames.add(lifeCycle.ephemeralPlace)
        if toName is None : toName = lifeCycle.ephemeralPlace  
        lifeCycle.placeNames.update(  fromNames )
        lifeCycle.placeNames.add( toName)
        self.name, self.fromNames, self.toName, self.negate = name, fromNames, toName, negate

class Configuration(object):
    @staticmethod
    def __getBooleanElementAttribute(element, attributeName, default=False):
        return "true" == element.get(attributeName, str(default)).lower()
    
    def __init__(self, Session, fileName):

        self.lifeCycles = dict()
        self.colours    = dict(((None, None),)) # { colourName : colour }
        self.lifeCyclesByColour = dict() # { colour: lifeCycle }
        self.places     = dict() # { (colour, placeName): place }
        self.liveSet = set()
        self.compatibilities = defaultdict(dict) #
        self.aliases = dict()
        self.actions = dict()
        
        with committing(Session) as session:
                
            elementTree = parse(fileName)
            
            for e1 in elementTree.findall('t:lifeCycle', _namespaceMap): 
                lifeCycle = _LifeCycle(self, e1.attrib['name'], e1.get('ephemeralState', 'ephemeral'))
                for e2 in e1.findall('t:stateChange', _namespaceMap):
                    _ProtoArc(lifeCycle, e2.attrib['name'], fromNames=e2.attrib['from'  ].split('|'), toName=e2.attrib['to'   ], negate=self.__getBooleanElementAttribute(e2, "negate")) 
                for e2 in e1.findall('t:condition', _namespaceMap):
                    _ProtoArc(lifeCycle, e2.attrib['name'], fromNames=e2.attrib['places'].split('|'),                            negate=self.__getBooleanElementAttribute(e2, "negate")) 
                for e2 in e1.findall('t:constructor', _namespaceMap):
                    _ProtoArc(lifeCycle, e2.attrib['name'],                                           toName=e2.attrib['place'], ) 
                for e2 in e1.findall('t:destructor', _namespaceMap):
                    _ProtoArc(lifeCycle, e2.attrib['name'], fromNames=[e2.attrib['place']], ) 
                    
            for e1 in elementTree.findall('t:compatibility', _namespaceMap):
                knotLifeCycle  = self.lifeCycles[e1.attrib['knotLifeCycle' ]] 
                otherLifeCycle = self.lifeCycles[e1.attrib['otherLifeCycle']] 
                compatibilityName = e1.attrib['name'] 
                for e2 in e1.findall('t:compatible', _namespaceMap):
                    otherStateName = e2.get('otherState', "NonExistent") # None means non-existing 
                    assert  otherStateName in otherLifeCycle.placeNames, (compatibilityName, otherStateName)
                    conditionName = e2.get('knotCondition') # None means 'unconditionally'
                    if conditionName is None:
                        compatibleNames = knotLifeCycle.placeNames
                    else:
                        condition = knotLifeCycle.arcs[conditionName]
                        if condition.negate:
                            compatibleNames = knotLifeCycle.placeNames - condition.fromNames
                        else: 
                            compatibleNames = condition.fromNames
                    self.compatibilities[compatibilityName][otherStateName] = compatibleNames 
                missingPlaceNames = otherLifeCycle.placeNames ^ set(self.compatibilities[compatibilityName])
                missingPlaceNames.discard("NonExistent")
                assert not missingPlaceNames, missingPlaceNames
            
            for entityE in elementTree.findall('t:entity', _namespaceMap):
                lifeCycle   = self.lifeCycles[entityE.attrib['lifeCycle'    ]]
                colourName = entityE.attrib['name']
                colour = self.colours[colourName] = Colour.get( session, #@UndefinedVariable
                                                                name=colourName,
                                                                ephemeralPlaceName=lifeCycle.ephemeralPlace,
                                                               )
                self.liveSet.add(colour) 
                self.lifeCyclesByColour[colour] = lifeCycle

                for placeName in lifeCycle.placeNames:
                    place = self.places[colour, placeName] = \
                        Place.get(session, colour=colour, name=placeName) #@UndefinedVariable
                    self.liveSet.add(place)
                    session.flush()
                    print place.colour.name, place.name, place.id
                     
                for aliasE in entityE.findall('t:alias', _namespaceMap):
                    aliasName = aliasE.attrib['name']
                    alias = self.aliases[aliasName] = Alias.get(session, name=aliasName, colour=colour) #@UndefinedVariable
                    
                for fieldE in entityE.findall('t:field', _namespaceMap):
                    try:             referencedColour = self.colours[fieldE.get('references')]
                    except KeyError: referencedColour = None
                    self.liveSet.add( Field.get(session, name=fieldE.get('name'), colour=colour, #@UndefinedVariable
                                                referencedColour=referencedColour))
                    if referencedColour: 
                        self.__adjustCompatibility(referencedColour , colour, fieldE.attrib['compatibility' ])
                        
                        for field in colour.fields.values():                        
                            self.liveSet.add( Linkage.get( session, #@UndefinedVariable
                                                           referencedAlias=self.aliases[aliasE.attrib[field.name]],
                                                           referencingAlias=alias,
                                                           field=field,
                                                           ))
                        
            strategies = dict(optimistic  = Action.LockingStrategy.optimistic ,
                              pessimistic = Action.LockingStrategy.pessimistic,
                              )
            for actionE in elementTree.findall('t:action', _namespaceMap):
                transfers = dict() # {name: arc}
                actionName = actionE.attrib['name']
                action = self.actions[actionName] = Action.get( session, 
                                     name=actionName,
                                     lockingStrategy= strategies[actionE.attrib['lockingStrategy']],
                                    )
                self.liveSet.add(action)
                             
                self.__createTransfers(session, actionE, action, transfers, xpath='t:transfer'    , arcAttributeNames={'t:extendScope':'true'}, )
                self.__createTransfers(session, actionE, action, transfers, xpath='t:new'         , arcAttributeNames={}, )
                self.__createTransfers(session, actionE, action, transfers, xpath='t:del'         , arcAttributeNames={'t:extendScope':'true'}, )
                self.__createTests(session, action, transfers)
                
            for transitionE in elementTree.findall('t:transition', _namespaceMap):
                transition = Transition.get(session, name=transitionE.attrib["name"]) #@UndefinedVariable
                actions = set()
                for doE in elementTree.findall('t:do', _namespaceMap):
                    actions.add(self.actions[doE.attrib["action"]])
                transition.actions = actions    
                       
            for Cls in Linkage, Arc, Place, Colour, Action, :
                try: isEmpty = Cls.isEmpty
                except AttributeError:
                    isEmpty = lambda x: True
                for obj in session.query(Cls):
                    if obj not in self.liveSet and isEmpty(obj):
                        session.delete(obj)    

    def __adjustCompatibility(self, referencedColour, otherColour, compatibilityName):
        if referencedColour is not None:
            print "---", referencedColour, otherColour
            compatibility = self.compatibilities[ compatibilityName ]
            for referencingPlace in otherColour.places:
                print referencingPlace, ":",
                for referencedPlace in referencedColour.places: 
                    if referencedPlace.name in compatibility[referencingPlace.name]:
                        print referencedPlace,
                        referencedPlace .compatibleReferencingPlaces.add(referencingPlace)
                    else:
                        referencedPlace .compatibleReferencingPlaces.discard(referencingPlace)
                print
                
    def __createTransfers(self, session, actionE, action, arcs, xpath, arcAttributeNames): 
        for arcE in actionE.findall(xpath, _namespaceMap):
            arcName = arcE.attrib['alias']
            kwargs = dict( (attributeName.split(':')[-1], arcE.attrib.get(attributeName, default)) for attributeName, default in arcAttributeNames.items() )
            try: alias = self.aliases[arcName]
            except KeyError as e:
                e.args = ('Reference to an unknown alias: "{key}"\n{element}'.format(key=e.args, element=tostring(arcE)),) 
                raise
            lifeCycle = self.lifeCyclesByColour[alias.colour] 
            for lifeCycleArcAttribute in 'stateChange', 'constructor', 'destructor', : #'condition', 
                try: protoArc = lifeCycle.arcs[arcE.attrib[lifeCycleArcAttribute]]
                except KeyError: pass
                else: 
                    fromNames = lifeCycle.placeNames - protoArc.fromNames if protoArc.negate else protoArc.fromNames
                    kwargs.update( froms = list( self.places[alias.colour, placeName] for placeName in fromNames),) 
                    kwargs.update( to = self.places[alias.colour, protoArc.toName ] )
                    break
            arc = arcs[arcName] = Transfer.get(session, action, alias=alias, **kwargs)
            self.liveSet.add(arc) 

    def __createTests(self, session, action, transfers):
        # first we need to find the linkages connecting the arcs
        linkages = set() 
        arcNames = set(transfers.keys())
        for transfer in transfers.values():            
            self.__processLinkage(session, action, linkages, transfer, arcNames, 
                                  Alias.referencedLinkages , Place.compatibleReferencedPlaces , Linkage.referencedAlias  ) #@UndefinedVariable
            self.__processLinkage(session, action, linkages, transfer, arcNames,
                                  Alias.referencingLinkages, Place.compatibleReferencingPlaces, Linkage.referencingAlias) #@UndefinedVariable

    @staticmethod
    def __get(descriptor, o): return descriptor.__get__(o, type(o))

    def __processLinkage(self, session, action, linkages, transfer, arcNames, linkagesAtt, compatiblePlacesAtt, aliasAtt):
        for linkage in self.__get(linkagesAtt, transfer.alias):
            alias = self.__get(aliasAtt, linkage)
            if linkage in linkages or alias.name in arcNames: continue 
            linkages.add(linkage)   
            arcNames.add(alias.name)
            compatibleWithFroms = set ((place for fro in transfer.froms
                                        for place in self.__get(compatiblePlacesAtt, fro) 
                                            if place.colour_id == alias.colour.id))
            compatibleWithTo = set( (place for place in self.__get(compatiblePlacesAtt, transfer.to) 
                                            if place.colour_id == alias.colour.id))   
            testPlaces = compatibleWithFroms & compatibleWithTo
            assert testPlaces
#            self.__createTest4Link(session, action, alias, testPlaces, compatibleWithTo   , compatibleWithFroms, reverse=False)
#            self.__createTest4Link(session, action, alias, testPlaces, compatibleWithFroms, compatibleWithTo   , reverse=True )
#            
#    def __createTest4Link(self, session, action, alias, testPlaces, compatibleWithTo, compatibleWithFroms, reverse):
            if "NonExistent" in (p.name for p in compatibleWithTo):
                inhibitedPlaces = (set(alias.colour.places) - compatibleWithTo) & compatibleWithFroms
                if inhibitedPlaces: 
                    #print "Inhibited by", alias.name, sorted(p.name for p in inhibitedPlaces)
                    self.liveSet.add(Inhibit.get(session, action=action, alias=alias, froms=sorted(inhibitedPlaces)))                    
            else:
                self.liveSet.add(Test.get(session, action=action, alias=alias, froms=sorted(testPlaces)))
        
if __name__ == "__main__":
            
    class Test2(unittest.TestCase):
        def test(self):
            engine = create_engine('sqlite:///:memory:', echo=False)
            createDb(engine)
            Session = sessionmaker(bind=engine)    
            Configuration(Session, "example.xml")
            with committing(Session) as session:
                for action in session.query(Action).all(): 
                    action._print()
                    q =  action.bindingQuery
                    print
                    print action
                    print q
                    print
                    for x in q.with_session(session).all(): 
                        print x
                
            Execution.start( Session )
    unittest.main()
